iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0
Modern Web

Hello TypeScript 菜鳥系列 第 25

Day 24. TypeScript Type What?:Narrowing、Type Guides

  • 分享至 

  • xImage
  •  

上一篇 提到type predicates是TypeScript裡面的一種narrowing技巧,今天就來看narrowing,以及同樣和narrowing有關係的type guard。


Narrowing 在TypeScript指的是作出"某些行為"讓TypeScripe compiler能將推導出的型別限縮至特定型別。

首先來看沒有narrowing會發生什麼事:

function concatMessage(msg: string | number) {
	return "Hello".concat(msg);
}

TypeScript沒有意外地給出了一個錯誤訊息:

Argument of type 'string | number' is not assignable to parameter of type 'string'.
Type 'number' is not assignable to type 'string'.(2345)

這裡刻意讓函式輸入參數可以是 stringnumber 型別,但因為 concat()string 型別用來串接 string 型別值的方法,而msg參數可能是 number 型別,所以compiler才會丟出msg不能被串接的錯誤訊息。

接著再宣告一個很無用的隨機數函式具體地來看到底什麼是narrowing?

function getRandomNumber(num: string | number) {
	const rand = Math.random();
	if(typeof num === "string"){
		return (rand * Math.round(rand)).toFixed().concat(num);
	}
	return Number(rand * Math.round(rand)) + num;
}

同樣地,這個範例設計成「若num參數是string 型別,才讓rand隨機數用 toFixed() 函式變成 string 型別去串接num參數」。

但不一樣的地方是,範例使用 if 條件式和 typeof 運算子確認num是不是 string 型別。

因此,即便num可能是 stringnumber 型別,TypeScript compiler仍會自動將if條件式區塊內的num參數視為 string 型別;反之,if條件式以外的num參數就會直接被compiler視為是 number 型別。

從這個範例可以看到compiler很聰明地將if條件式內的num參數限縮至是 string 型別,這種將可能的型別限縮至特定(幾個)型別的動作就稱為「narrow」(名詞就是narrowing)。

另外,因為 typeof 運算子能幫忙檢查(或者說限縮)變數的型別(an expression to narrow down potential types),因此 typeof 運算子就被稱為是一種「type guard」。


文件中提到有關narrowing的方式大致分為以下幾種:

  • Control flow analysis
  • Assignments (type inference)
  • Truthiness narrowing ( !, &&, ||, etc. )
  • Equality narrowing str !== null
  • Type guards
    • typeof operator
    • in operator
    • instanceof operator
    • type predicates (a user-defined type guard)

如果熟悉JavaScript就會熟知如何使用這些方式去限縮被compiler推導出的型別,所以這篇文章的目的只是簡單介紹並舉例如何使用這些技巧去限縮型別。

Control flow analysis 條件控制分析

條件控制分析就如同上面的 getRandomNumber函式範例,利用if、if-else等條件式讓TypeScript去限縮變數可能的型別範圍,而條件式判斷不一定要像前面範例一樣得加入type guards:

let y: unknown;
const rand = Math.random();

if(rand < 0.5) {
	y = "less than 0.5";
} else {
	y = 1;
}

console.log(rand, typeof y);	// y is either string or number type

Assignments 賦值

事實上在Typescript裡,賦值本身就是一種narrowing,因為compiler會透過型別推導(type inference)的方式去推敲出變數可能的型別。

如同下面範例(1)的變數x就會透過賦值字串值,就會被推導並限縮成是 string 型別;同樣地,範例(2)變數y的型別則會被限縮成可能是 stringnumber 型別(string | number)。

// (1)
let x = "a string variable x";
x = 1;	// error

// (2)
let y = Math.random() < 0.5 ? "less than 0.5" : 1;	// y is either string or number type

Truthiness narrowing

Truthiness narrowing 是藉由truthy和falsy value來限縮型別,而TypeScript的falsy value就和JavaScript一樣只有以下這幾種:

  • 0
  • NaN
  • ""、''、`` (empty string)
  • 0n
  • null
  • undefined

除此之外都是truthy value。

而任何跟判斷truthy和falsy value有關的語法都算是truthiness narrowing,像是 &&||Boolean()!、if條件式等。

稍微修改一下getRandomNumber例子:

function getRandomNumber(num?: string | number) {
	const rand = Math.random();
	if(num && typeof num === "string"){
		return (rand * Math.round(rand)).toFixed().concat(num);
	} else if (num && typeof num === "number") {
		return Number(rand * Math.round(rand)) + num;
	} else	{
		return `num is undefined`;
	}
}

這個例子讓num的型別可能是 string | number | undefined,為了避免對 undefined 型別的num有任何運算,所以特地加上 && 運算子讓num不是 undefined 型別的時候,可根據是 string 還是 number 決定是要處理哪個區塊的運算。

Equality narrowing

請注意,前一個例子的num若是 空字串(empty string) 或是 0 都會回傳 num is undefined,這種情況是不樂見的。

這時候可以用等號運算子來判斷num是否為undefined,而利用 =====!=!== 運算子來限縮型別的方式就是equality narrowing:

function getRandomNumber(num?: string | number) {
	const rand = Math.random();
	const numIsDefiend = typeof num !== 'undefined';	// 這行會確認num是否為undefined
	
	if(numIsDefiend && typeof num === "string"){
		return (rand * Math.round(rand)).toFixed().concat(num);
	} else if (nnumIsDefiend && typeof num === "number") {
		return Number(rand * Math.round(rand)) + num;
	} else	{
		return `num is undefined`;
	}
}

Type guards

TypeScript文件中的type guards大致可整理成以下這幾個:

其中 type predicates 在前一篇已介紹過,而 typeofininstanceof 運算子的用法和JavaScript語法完全相同,這邊就不多贅述了。

只是要注意 in 運算子只能判斷物件是否有某個屬性,無法判斷物件是否由某個class產生;若要判斷物件是否由某個class產生或是否繼承某個原型鏈(prototype chain),就得使用instanceof

最後有個關於屬性的narrowing要特別提一下discriminated unions概念:

interface Animal {
	specie: "human" | "gorilla";
	name?: string;
	gorillaId?: number;
}

function getHumanName(animal: Animal) {
  if(animal.specie === "human"){
	alert(animal.name.concat(` is ${animal.specie}`));	// error
  }
}

範例運用control flow analysis限縮參數animal應顯示哪種屬性 ─ 「如果animal的specie屬性是 "human",則將name串接specie屬性並顯示出來」。

結果很不幸的是得到以下錯誤訊息:

Object is possibly 'undefined'.(2532)

因為已經定義Animal的name屬性可能是undefined,這種情況下compiler仍然可能無法判斷animal是否存在name屬性。

但文件有說明最好不要用non-null assertion operator ! 改成 animal.name! 去斷言name屬性存在,若程式碼出現預期以外的錯誤,! 可能不會拋出錯誤訊息。

文件說明因為事先知道animal是human才有name屬性、animal是gorilla則不會有name屬性,所以應該區分成擁有共同屬性的兩種型別,,再union成一種型別來解決,而這種方式稱作discriminated unions。

將範例套用discriminated unions的作法如下:

interface Human {
	specie: "human";
	name: string;
}
interface Gorilla {
	specie: "gorilla";
	gorillaId: number
}
type Animal = Human | Gorilla;

function getHumanName(animal: Animal) {
  if(animal.specie === "human"){
	alert(animal.name.concat(` is ${animal.specie}`));	// ok
  }
}

參考資料
Narrowing @TypeScript Handbook
TypeScript Type Guards @TypeScript Tutorial
Aha! Understanding Typescript’s Type Predicates
TypeScript Type Guards and Type Predicates


上一篇
Day 23. TypeScript Type What?:Type Predicates
下一篇
Day 25. TypeScript Type Manipulation: & Operator、typeof Operator、keyof Operator
系列文
Hello TypeScript 菜鳥31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言